﻿using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace System.IO
{
    public class LongPathTools : IPathTools
    {
        public static LongPathTools Instance = new LongPathTools();

        public string GetParentDirectory(string path)
        {
            return GetDirectoryName(path);
        }

        public string ChangeExtension(string path, string extension)
        {
            var components = GetPathComponents(path);
            var last = components[components.Count - 1];
            var lastIndexOf = last.LastIndexOf('.');
            if (lastIndexOf == -1) return path;
            if (extension.StartsWith(".")) extension = extension.Substring(1);
            components[components.Count - 1] = last.Substring(0, lastIndexOf) + "." + extension;
            return Combine(components.ToArray());
        }

        public string GetDirectoryName(string path)
        {
            if (path == null) return null;

            var components = GetPathComponents(path);

            if (GetPathFormat(path) != PathFormat.Relative && components.Count == 1)
                return null;

            return Combine(components.Take(components.Count - 1).ToArray());
        }

        private static char[] GetInvalidPathCharsImpl()
        {
            return Path.GetInvalidPathChars();
        }

        public char[] GetInvalidPathChars()
        {
            return GetInvalidPathCharsImpl();
        }

        public char[] GetInvalidFileNameChars()
        {
            return Path.GetInvalidFileNameChars();
        }

        public string GetExtension(string path)
        {
            var components = GetPathComponents(path);
            var last = components[components.Count - 1];
            var lastIndexOf = last.LastIndexOf('.');
            if (lastIndexOf == -1) return path;
            return last.Substring(lastIndexOf + 1);
        }

        public string GetFullPath(string path)
        {
            return CheckAndNormalize(path);
        }

        public string GetFileName(string path)
        {
            var components = GetPathComponents(path);
            return components[components.Count - 1];
        }

        public string GetFileNameWithoutExtension(string path)
        {
            var components = GetPathComponents(path);
            var last = components[components.Count-1];
            var lastIndexOf = last.LastIndexOf('.');
            if (lastIndexOf == -1) return path;
            components[components.Count - 1] = last.Substring(0, lastIndexOf);
            return Combine(components.ToArray());
        }

        public string GetPathRoot(string path)
        {
            return GetPathComponents(path).First();
        }

        public string GetTempPath()
        {
            return Path.GetTempPath();
        }

        public string GetRandomFileName()
        {
            return Path.GetRandomFileName();
        }

        public string GetTempFileName()
        {
            return Path.GetTempFileName();
        }

        public bool HasExtension(string path)
        {
            return GetPathComponents(path).Last().LastIndexOf('.') != -1;
        }

        public bool IsPathRooted(string path)
        {
            return GetPathFormat(path) != PathFormat.Relative;
        }

        public string Combine(params string[] paths)
        {
            return Combine((IEnumerable<string>)paths);
        }

        public string Combine(IEnumerable<string> paths)
        {
            var components = new List<string>();

            foreach (var path in paths)
            {
                var pathComponents = GetPathComponents(path);
                components.AddRange(pathComponents);
            }

            return string.Join(new string(DirectorySeparator, 1), components);
        }


        public static void ThrowIfPathEmptyOrHasInvalidCharacters(string path)
        {
            if (path == null) throw new ArgumentNullException("path");
            if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("path");
            if (path.IndexOfAny(GetInvalidPathCharsImpl()) != -1)
            {
                if (path.Length <= 2 || path[2] != '?') throw new ArgumentException("path");

                path = path.ReplaceAt('_', 2);
                if (path.IndexOfAny(GetInvalidPathCharsImpl()) != -1) throw new ArgumentException("path");
            }
        }

        private static char DirectorySeparator { get { return '\\'; } }
        private static char AlternativeDirectorySeparator { get { return '/'; } }

        /// <summary>
        /// Replaces multiple DirectorySeparator occurences with single
        /// </summary>
        private static string FixMultiplePathSlashes(string path, int startFrom)
        {
            bool previousSlash = false;
            var builder = new StringBuilder();
            for (int i = startFrom; i < path.Length; i++)
            {
                if (path[i] == DirectorySeparator || path[i] == AlternativeDirectorySeparator)
                {
                    if (!previousSlash && i != path.Length - 1) builder.Append(DirectorySeparator);
                    previousSlash = true;
                }
                else
                {
                    previousSlash = false;
                    builder.Append(path[i]);
                }
            }

            return path.Substring(0, startFrom) + builder;
        }

        private enum PathFormat
        {
            /// <summary>
            /// Folder\file.txe
            /// </summary>
            Relative,

            /// <summary>
            /// C:\Folder\file.txt
            /// C:Folder\
            /// </summary>
            Absolute,

            /// <summary>
            /// \\ComputerName\SharedFolder\Resource
            /// </summary>
            Network,

            /// <summary>
            /// \\?\C:\Folder\Resource
            /// </summary>
            Unc,

            /// <summary>
            /// Also mentioned as 'long unc'.
            /// \\?\UNC\ComputerName\ShareName
            /// </summary>
            NetworkUnc,
        }

        private class PathsComparerClass : IEqualityComparer<string>
        {
            public static PathsComparerClass Instance  = new PathsComparerClass();

            public bool Equals(string x, string y)
            {
                return StringComparer.OrdinalIgnoreCase.Equals(x, y);
            }

            public int GetHashCode(string obj)
            {

                return StringComparer.OrdinalIgnoreCase.GetHashCode(obj);
            }
        }

        public static StringComparison PathComparison { get { return StringComparison.OrdinalIgnoreCase; } }

        private static PathFormat GetPathFormat(string path)
        {
            if (path.Length > 1 && path[1] == ':' && StartsWithAny(path, "abcdefghijklmnopqrstuvwxyz", PathsComparerClass.Instance))
                return PathFormat.Absolute;
            if (path.StartsWith(@"\\?\UNC\"))
                return PathFormat.NetworkUnc;
            if (path.StartsWith(@"\\?\"))
                return PathFormat.Unc;
            if (path.StartsWith(@"\\"))
                return PathFormat.Network;

            return PathFormat.Relative;
        }

        private static bool StartsWithAny(string s, IEnumerable<char> with, IEqualityComparer<string> comparer)
        {
            if (string.IsNullOrEmpty(s)) return false;
            foreach (var c in with)
            {
                if (comparer.Equals(s.Substring(0, 1), new string(c, 1))) return true;
            }
            return false;
        }

        public static string CheckAndNormalize(string path, bool normalizeUnc = false)
        {
            ThrowIfPathEmptyOrHasInvalidCharacters(path);
            var format = GetPathFormat(path);

            int prefixLength = 0;
            if (format == PathFormat.Network)
                prefixLength = 2;
            if (format == PathFormat.Unc)
                prefixLength = 4;
            if (format == PathFormat.NetworkUnc)
                prefixLength = 8;

            path = FixMultiplePathSlashes(path, prefixLength);

            string serverName = string.Empty;

            if ((format == PathFormat.Unc || format == PathFormat.NetworkUnc) && !normalizeUnc) return path;

            if (format == PathFormat.Network || format == PathFormat.NetworkUnc)
            {
                int shareNameIndex = path.IndexOf(DirectorySeparator, prefixLength + 1);
                serverName = path.Substring(0, shareNameIndex);
                if (shareNameIndex == -1) throw new ArgumentException("Network path must contain share name.");
                prefixLength = shareNameIndex;
            }
            
            // use startFrom to skip prefix of server name
            var components = path.Substring(prefixLength).Split(new[] { DirectorySeparator }, StringSplitOptions.RemoveEmptyEntries);
            var componentsStack = new Stack<string>();
            for (int i = 0; i < components.Length; i++)
            {
                if (components[i] == ".") continue;
                if (components[i] == "..") componentsStack.Pop();
                else componentsStack.Push(components[i]);
            }

            var finalComponentsList = new List<string>();

            while (componentsStack.Count > 0)
            {
                finalComponentsList.Add(componentsStack.Pop());
            }

            string joinedComponents = string.Join(new string(DirectorySeparator, 1), Enumerable.Reverse(finalComponentsList));
            if (!string.IsNullOrEmpty(serverName))
                joinedComponents = serverName + DirectorySeparator + joinedComponents;

            if (format == PathFormat.Unc)
                return @"\\?\" + joinedComponents;
            // for network unc it prefix is added as part of serverName

            if (format == PathFormat.Absolute && joinedComponents.Length > MAX_PATH)
                return @"\\?\" + joinedComponents;

            if (format == PathFormat.Network && joinedComponents.Length > MAX_PATH)
                return @"\\?\UNC\" + joinedComponents;

            return joinedComponents;
        }

        private const int MAX_PATH = 260;

        /// <summary>
        /// Splits path into components, which are: root directory, directories and file (which in fact makes no difference with directory).
        /// </summary>
        public IList<string> GetPathComponents(string path, bool includePrefixes = true)
        {
            path = CheckAndNormalize(path);
            var format = GetPathFormat(path);

            int prefixLength = 0;
            if (format == PathFormat.Network)
                prefixLength = 2;
            if (format == PathFormat.Unc)
                prefixLength = 4;
            if (format == PathFormat.NetworkUnc)
                prefixLength = 8;

            var result = path.Substring(prefixLength).Split(new[] {DirectorySeparator}, StringSplitOptions.RemoveEmptyEntries);
            if (includePrefixes)
            {
                if (format == PathFormat.Network)
                    result[0] = @"\\" + result[0];
                if (format == PathFormat.Unc)
                    result[0] = @"\\?\" + result[0];
                if (format == PathFormat.NetworkUnc)
                    result[0] = @"\\?\UNC\" + result[0];
            }
            return result;
        }
    }
}